package vh.sphynx.runtime.interpreter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import vh.sphynx.runtime.ExecutionContext;
import vh.sphynx.runtime.SphynxInterpreter;
import vh.sphynx.runtime.VariableStorage;
import vh.sphynx.runtime.interpreter.XFunctionDeclartationInterpreter.FunctionReturn;
import vh.sphynx.runtime.pattern.CompositePatternMatchInfo;
import vh.sphynx.runtime.pattern.IPatternMatchInfo;
import vh.sphynx.runtime.pattern.PatternMatchInfo;
import vh.sphynx.sphynx.CallParameter;
import vh.sphynx.sphynx.FunctionArgument;
import vh.sphynx.sphynx.SphynxFactory;
import vh.sphynx.sphynx.XExpression;
import vh.sphynx.sphynx.XFeatureCall;
import vh.sphynx.sphynx.XFunctionDeclaration;
import vh.sphynx.sphynx.XJavaTypeImport;
import vh.sphynx.sphynx.XPatternClassDeclaration;
import vh.sphynx.sphynx.XPatternDeclaration;
import vh.sphynx.sphynx.XReturnStatement;
import vh.sphynx.sphynx.impl.XFeatureCallImpl;
import vh.sphynx.util.CharSequenceStream;
import vh.sphynx.util.PatternMismatchedException;
import vh.sphynx.util.SphynxNamedPassRequiredException;
import vh.sphynx.util.SphynxRuntimeException;

public class PatternMatcher implements RuntimeErrorMessage {
	
	public static final String OPT_MIN_OCCUR = "minOccur";
	public static final String OPT_MAX_OCCUR = "maxOccur";
	public static final String OPT_RETURN_LAST_RESULT = "returnLastMatch";
	public static final String OPT_CLOSE_INPUT = "closeInput";
	public static final String OPT_EXCEPTION_ON_MISMATCH = "mismatchedException";
	public static final String OPT_POSITION = "position";
	public static final String OPT_UPDATE_PARENT_POSITION = "updateParentPosition";
	
	private SphynxInterpreter interpreter;
	private XPatternDeclaration funcDef;
	private XPatternClassDeclaration patternDef;
	private ExecutionContext calleeContext;
	
	private static final XPatternClassDeclaration DEFAULT_PATTERN_CLASS = createDefaultPatternClass(); 

	public Object matchPattern(SphynxInterpreter interpreter, XPatternDeclaration funcDef, XFeatureCall featureCall,
			ExecutionContext context, ExecutionContext callContext) {					
		this.interpreter = interpreter;
		this.funcDef = funcDef;
		this.patternDef = funcDef.getPatternType();
		if (patternDef == null)
			patternDef = context.getVariableValueWithDefault("DefaultPatternCLass", null);
		
		if (patternDef == null)
			patternDef = DEFAULT_PATTERN_CLASS;
		
		calleeContext = callContext != null ? callContext : context.createCalleeContext();
		calleeContext.setExecutingFunction(funcDef);
		
		passParameters(interpreter, funcDef, featureCall, context, calleeContext);		
		
		int minOccur = getOption(OPT_MIN_OCCUR, 1);
		int maxOccur = getOption(OPT_MAX_OCCUR, 1);
		int closeInput = getOption(OPT_CLOSE_INPUT, -1);
		boolean returnLastMatch = getOption(OPT_RETURN_LAST_RESULT, true);
		boolean stopOnError = getOption(OPT_EXCEPTION_ON_MISMATCH, true);
		
		int occurence = 0;
		IPatternMatchInfo parentMatchInfo = getMatchInfo(context);
		IPatternMatchInfo lastMatchInfo = null;
		List<Object> matchedList = null;
		Object singleResult = null;
		try {
			if (!returnLastMatch)
				matchedList = new ArrayList<Object>();
			while(true) {
				try {
					IPatternMatchInfo matchInfo = createMatchInfo();
					singleResult = singleMatch(matchInfo, parentMatchInfo, occurence, lastMatchInfo != null ? lastMatchInfo.getGlobalEndPosition() : 0);
					lastMatchInfo = matchInfo;					
					occurence++;
					if (!returnLastMatch) 
						matchedList.add(singleResult);
					if (occurence >= maxOccur)
						break;
				}
				catch(RuntimeException e) {
					if (stopOnError)
						throw e;
					break;
						/*throw new PatternMismatchedException(context, funcDef,
								String.format("Error matching patternn '%s' at times %d", funcDef.getName(), occurence), e);*/
				}
				
				
			} //while (occurence < maxOccur && lastMatchInfo.isMatched());
			
		} catch (RuntimeException e) {
			// get cause
			Throwable cause = e;
			while (cause.getCause() != null) cause = cause.getCause();
		
			//e.printStackTrace(); s
			
			if (cause instanceof PatternMismatchedException) {
				if (occurence < minOccur && stopOnError)
					throw (RuntimeException)cause;
					/*throw new PatternMismatchedException(context, funcDef,
							String.format("Pattern '%s' is matched only %d time(s), while expected %d times.", 
									 funcDef.getName(), occurence, minOccur), e);*/
			}
			else throw e;
			
		} finally {
			CharSequence input = lastMatchInfo != null ? lastMatchInfo.getGlobalInput() : null;
			CharSequence pInput = parentMatchInfo != null ? parentMatchInfo.getGlobalInput() : null;
			
			if (input instanceof CharSequenceStream && closeInput != 0) {
				if (closeInput > 0 || input != pInput) {
					((CharSequenceStream) input).close();
				}
			}
		}
			
		return returnLastMatch ? (singleResult!=null ? singleResult : PatternMatchInfo.FALSE_MATCHED):	matchedList;
	}

	private static XPatternClassDeclaration createDefaultPatternClass() {
		XPatternClassDeclaration def = SphynxFactory.eINSTANCE.createXPatternClassDeclaration();
		XJavaTypeImport defType = SphynxFactory.eINSTANCE.createXJavaTypeImport();
		FunctionArgument arg = SphynxFactory.eINSTANCE.createFunctionArgument();
		
		defType.setClass(CompositePatternMatchInfo.class.getName());
			
		arg.setName("input");
		arg.setValue(SphynxFactory.eINSTANCE.createXNullLiteral());
				
		def.setPatternType(defType);
		def.getArguments().add(arg);
				
		return def;
	}

	private  <T> T getOption(String key, T defaultValue) {
		/*Object result;
		if (options != null && options.containsKey(key))
			result = options.get(key);
		else
			result = defaultValue;
		return (T)result;*/
		return calleeContext.getVariableValueWithDefault(key, defaultValue);
	}

	private Object singleMatch(IPatternMatchInfo matchInfo, IPatternMatchInfo parentMatchInfo, int occurrence, int lastEndLocation) {		
		matchInfo.setOccurrenceIndex(occurrence);
		
		CharSequence input = calleeContext.getVariableValueWithDefault("input", null);
		if (input == null)
			input = parentMatchInfo.getGlobalInput();
		boolean useParentInput = parentMatchInfo != null && input == parentMatchInfo.getGlobalInput();
		
		// update global offset
		int globalOffset = 0;
		if (occurrence == 0)
			globalOffset = useParentInput ? parentMatchInfo.getGlobalPosition() : getOption(OPT_POSITION, 0);
		else
			globalOffset = lastEndLocation;	
		//int globalOffset = useParentInput && occurrence == 0 ? parentMatchInfo.getGlobalOffset() : lastEndLocation;
		
		//if (occurrence == 0) // first match
		//	globalOffset += getOption(OPT_POSITION, useParentInput ? parentMatchInfo.getPosition() : 0);
		//else if (useParentInput)
		//	globalOffset += parentMatchInfo.getPosition();
		
		int endOffset = useParentInput ? parentMatchInfo.getGlobalEndPosition() : input.length();
		
			
		matchInfo.setGlobalInput(input);
		matchInfo.setGlobalOffset(globalOffset);
		matchInfo.setBeginLocation(0);
		matchInfo.setEndLocation(endOffset - globalOffset);
		matchInfo.setUseParentInput(useParentInput);
		
		// create 'this' entry in the context
		ExecutionContext tempContext = calleeContext.createDependentContext();
		// TODO check the following line, which might lead to unexpected behavior
		// tempContext.setExecutingFunction(calleeContext.getExecutingFunction());		
		tempContext.allocateVariable(VariableStorage.READONLY, false, "this", matchInfo);
		tempContext.setThisObject(matchInfo);
		matchInfo.intialize();
		Object result = null;
		
		
		try {
			if (funcDef.getBody() != null) {
				result = interpreter.resolveValue(funcDef.getBody(),  tempContext);
				if (result instanceof FunctionReturn)
					result = ((FunctionReturn) result).Result;
				/*if (result instanceof XReturnStatement)
					result = interpreter.resolveValue(((XReturnStatement) result).getValue(), tempContext);
				if (result instanceof VariableStorage)
					result = ((VariableStorage) result).getValue();*/
			}
		}
		catch (Exception e) {
			if (funcDef.getException() != null)
				result = interpreter.resolveValue(funcDef.getException(), tempContext);
			else if (e instanceof RuntimeException)
				throw (RuntimeException)e;
			else throw new PatternMismatchedException(tempContext, funcDef, e);
		}
					
		// update the parentMatchInfo position
		if (useParentInput && getOption(OPT_UPDATE_PARENT_POSITION, true)) {
			parentMatchInfo.updateEndLocation(matchInfo.getEndLocation());
			parentMatchInfo.setPosition(matchInfo.getGlobalEndPosition() - parentMatchInfo.getGlobalOffset(), IPatternMatchInfo.SEEK_GLOBAL_FROM_BEGIN);
		}
		return result != null ? result : matchInfo;
	}

	@SuppressWarnings("unchecked")
	private IPatternMatchInfo createMatchInfo() {
		IPatternMatchInfo matchInfo;
		XPatternClassDeclaration type = patternDef;
		if (type != null) {
			Class<? extends IPatternMatchInfo> clazz = (Class<? extends IPatternMatchInfo>) 
					interpreter.resolveValue(type.getPatternType(), calleeContext);
			try {
				matchInfo = clazz.newInstance();
			} catch (Exception e) {
				throw new SphynxRuntimeException(calleeContext, type.getPatternType(), "Pattern type is unknown.", e);
			}
		}
		else matchInfo = new CompositePatternMatchInfo();
		//matchInfo.setPatternDef(funcDef);
		matchInfo.setContext(calleeContext);
		return matchInfo;
	}

	private static IPatternMatchInfo getMatchInfo(ExecutionContext context) {
		VariableStorage thisVar = context.getVariable("this", false, false);
		if (thisVar != null && thisVar.getValue() instanceof IPatternMatchInfo)
			return (IPatternMatchInfo) thisVar.getValue();
		return null;
	}
	
	private void passParameters(SphynxInterpreter interpreter,
			XFunctionDeclaration func, XFeatureCall call, ExecutionContext context,
			ExecutionContext callContext) {
				
		Map<String, XExpression> argsMap = new HashMap<String, XExpression>();
		//List<VariableStorage> args = new ArrayList<VariableStorage>();
		// 0th round, allocate for pattern argument
		for(XExpression e : patternDef.getArguments()) {
			FunctionArgument arg = (FunctionArgument) e;
			callContext.allocateVariable(VariableStorage.ARGUMENT, false, arg.getName(), VariableStorage.EMPTY);
			argsMap.put(arg.getName(), arg.getValue());
		}
		
		// 1st round, allocation for arguments of pattern
		int idx = 0; 
		boolean allowAnyPass = true; // a pattern by default supports any pass
		boolean namePassed = false;
		for(XExpression e : func.getArguments()) {
			FunctionArgument arg = (FunctionArgument) e;
			if (arg.isAny())
				allowAnyPass = true;
			else if (arg.getName() != null) {
				callContext.allocateVariable(VariableStorage.ARGUMENT, false, arg.getName(), VariableStorage.EMPTY);
				argsMap.put(arg.getName(), arg.getValue());
				namePassed = true;
			} else if (!namePassed) {
				Object value = interpreter.resolveValue(arg.getValue(), context);
				callContext.getVariable(patternDef.getArguments().get(idx).getName(), false, false).setValue(value);
				idx++;
			}
			else 
				throw new SphynxNamedPassRequiredException(context, call); 
			
		}
		// 2nd round, matching arguments
		namePassed = false;
		boolean anyPassed = false;
		List<Object> anyArgs = allowAnyPass? new ArrayList<Object>() : null;
		
		if (call != null)
			for(CallParameter param: call.getParams()) {
				Object value = param.getValue() != null ? interpreter.resolveValue(param.getValue(), context) : null;
				if (param.isInheritedParameters())
					if (anyPassed) 
						throw new SphynxRuntimeException(context, call, String.format(PARAMETER_ALDREADY_PASSED, param.getName()));
					else anyPassed = true;
				else if (param.getName() == null) // passed by location
					if (namePassed)
						throw new SphynxNamedPassRequiredException(context, call);
					else if (idx < func.getArguments().size()) {
						callContext.getVariable(func.getArguments().get(idx).getName(), false, false).setValue(value);
						idx++;
					}
					else if (allowAnyPass) {
						anyArgs.add(value); // pass as __args
					}
					else
						throw new SphynxRuntimeException(context, call, TOO_MANY_PARAMETER_PASSING);
				else { // passed by name
					namePassed = true;
					VariableStorage var = callContext.getVariable(param.getName(), false, false);
					if (var != null)
						if (var.isEmpty())
							var.setValue(value);
						else
							throw new SphynxRuntimeException(context, call, String.format(PARAMETER_ALDREADY_PASSED, param.getName()));
					else if (allowAnyPass) { 
						anyArgs.add(value);
						callContext.allocateVariable(VariableStorage.UNUSED_ARGUMENT, false, param.getName(), value);
					}
					else
						throw new SphynxRuntimeException(context, call, String.format(UNDECLARED_ARGUMENT_PASSED, param.getName()));
				}
				idx++;	
			}
		// 3rd round, passing 'any' parameters
		if (anyPassed) {
			for (String argName: context.getVariableNames(VariableStorage.UNUSED_ARGUMENT)) {
				VariableStorage var = callContext.getVariable(argName, false, false);
				VariableStorage arg = context.getVariable(argName, false, false);
				if (var != null) {
					if (var.isEmpty())
						var.setValue(arg.getValue());
				}
				else if (allowAnyPass) 
					callContext.allocateVariable(VariableStorage.UNUSED_ARGUMENT, false, argName, arg.getValue());
			}
		}
		//4th round, check for uninitialized parameters
		for(String argName: argsMap.keySet()) {
			VariableStorage arg = callContext.getVariable(argName, false, false);
			if (arg.isEmpty()) {
				XExpression value = argsMap.get(argName);
				if (value != null)
					arg.setValue(interpreter.resolveValue(value, context));
				else 
					throw new SphynxRuntimeException(context, call, 
							String.format(UNINITIALIZED_ARGUMENT, argName));
			}
		}		
		if (anyArgs != null)
			callContext.allocateVariable(VariableStorage.READONLY, false, "__args", anyArgs.toArray());
	}
}